<template>
  <basic-dialog
    title="图形验证"
    width="384px"
    class="verify-dialog"
    confirm-text="确认"
    :visible="visible"
    :confirm="validate"
    :disabled-confirm="!state.dots.length"
    @update:visible="visible = $event"
    @close="handleDialogClose"
  >
    <div class="top">
      <div class="label">请在下图<span class="text-[red]">依次</span>点击:</div>
      <el-image :src="state.thumb"></el-image>
      <el-button
        class="w-[32px] h-[32px]"
        :disabled="fetchLoading"
        @click="() => reset()"
      >
        <basic-icon
          :class="{ 'loading-anim': fetchLoading }"
          name="refresh"
          color="#595E70"
        ></basic-icon>
      </el-button>
    </div>
    <div
      ref="verifyContentRef"
      class="verify-content"
      :style="{ width: width + 'px', height: height + 'px' }"
    >
      <img :src="state.bg" class="absolute w-full h-full" />

      <!-- 轴线 测试用 -->
      <template v-if="false">
        <div
          class="x-axis axis"
          :style="{ top: state.mouseY - 1 + 'px' }"
        ></div>
        <div
          class="y-axis axis"
          :style="{ left: state.mouseX - 1 + 'px' }"
        ></div>
      </template>

      <!-- 点 -->
      <div class="dots">
        <div
          v-for="(item, index) in state.dots"
          :key="index"
          class="dot"
          :style="buildDotStyle(item)"
        >
          {{ index + 1 }}
        </div>
      </div>

      <!-- 提示信息 -->
      <transition name="slide-bottom">
        <div
          v-show="state.showStatus"
          class="verify-tips"
          :class="{
            'verify-tips__success': state.status === 1,
            'verify-tips__error': state.status === 2
          }"
        >
          {{ verifyMsg }}
        </div>
      </transition>

      <!-- loading -->
      <transition name="fade">
        <div v-show="loading" class="loading">
          <img class="icon" src="./loading.svg" />
          <div class="text">验证中，请稍等...</div>
        </div>
      </transition>

      <!-- 遮罩层，鼠标相关做作都在这一层，防止元素打断 -->
      <div
        class="mask"
        @click="handlePushDot"
        @mousemove="handleMouseMove"
        @mouseleave="handleMouseLeave"
      ></div>
    </div>
  </basic-dialog>
</template>

<script setup lang="ts">
type Point = [number, number]

interface IEmit {
  (event: 'success', key: any): void
}

interface IProps {
  init: () => Promise<{ image: string; thumb: string; key: string }>
  // 验证函数，验证失败直接在函数中抛出错误。
  verify: (e: { dots: string; key: string }) => Promise<{ key?: string }>
  // 区域实际宽度
  width?: number
  // 区域实际高度
  height?: number
  // 点大小
  dotSize?: number
  // 文字最大数量
  textTotal?: number
  // 自动验证，满textTotal最大数量时自动验证
  auto?: boolean
}

const emit = defineEmits<IEmit>()
const props = withDefaults(defineProps<IProps>(), {
  width: 328,
  height: 248,
  dotSize: 20,
  textTotal: 4,
  auto: true
})

const visible = ref(false)
const loading = ref(false)
const image = ref<HTMLImageElement>(document.createElement('img'))
const verifyContentRef = ref<HTMLElement>()

const state = reactive({
  bg: '',
  thumb: '',
  key: '',
  showStatus: false,
  status: -1,
  imageWidth: 0,
  imageHeight: 0,
  mouseX: -1,
  mouseY: -1,
  dots: [] as Point[]
})

const verifyMsg = computed(() => {
  return state.status === 1 ? '验证成功' : '验证失败'
})

// 重置
async function reset(reFetch = true) {
  state.showStatus = false
  state.dots = []
  setTimeout(() => {
    state.status = -1
    state.mouseX = -1
    state.mouseY = -1
  }, 300)
  if (reFetch) {
    await fetchVerifyData()
  }
}

// push点位
async function handlePushDot(e: MouseEvent) {
  if (state.dots.length >= props.textTotal) {
    return
  }
  state.dots.push([e.offsetX, e.offsetY])
  if (state.dots.length === props.textTotal && props.auto) {
    validate()
  }
}

// 验证
async function validate() {
  const arr = state.dots.map(dot => {
    const xRate = props.width / state.imageWidth
    const yRate = props.height / state.imageHeight
    return [dot[0] / xRate, dot[1] / yRate]
  })
  if (!props.verify) {
    throw new Error('缺少validate验证函数')
  }
  try {
    const dots = arr.map(item => item.join(',')).join(',')
    loading.value = true
    const ret = await props.verify({ dots, key: state.key })
    state.status = 1
    setTimeout(() => {
      emit('success', ret)
      visible.value = false
    }, 1000)
  } catch (error) {
    state.status = 2
    throw error
  } finally {
    loading.value = false
    state.showStatus = true
    setTimeout(() => {
      reset()
    }, 1500)
  }
}

function buildDotStyle(dot: [number, number]) {
  const offset = props.dotSize / 2
  return {
    width: props.dotSize + 'px',
    height: props.dotSize + 'px',
    top: dot[1] - offset + 'px',
    left: dot[0] - offset + 'px'
  }
}

// 鼠标移入区域
function handleMouseMove(e: MouseEvent) {
  state.mouseX = e.offsetX
  state.mouseY = e.offsetY
}

// 鼠标离开区域
function handleMouseLeave() {
  state.mouseX = -1
  state.mouseY = -1
}

const fetchLoading = ref(false)
async function fetchVerifyData() {
  fetchLoading.value = true
  const res = await props.init()
  state.bg = res.image
  state.thumb = res.thumb
  state.key = res.key
  image.value.src = res.image
  image.value.onload = () => {
    state.imageWidth = image.value!.width
    state.imageHeight = image.value!.height
  }
  fetchLoading.value = false
}

async function open() {
  await fetchVerifyData()
  visible.value = true
}

function handleDialogClose() {
  visible.value = false
  reset(false)
}

defineExpose({ validate, open })
</script>

<style scoped lang="scss">
@keyframes loading {
  from {
    transform: rotate(0);
  }

  to {
    transform: rotate(365deg);
  }
}

.loading-anim {
  animation: loading 1s infinite linear reverse;
}

.verify-dialog {
  .top {
    @apply flex items-center;

    .el-image {
      @apply flex-1 h-[32px] mx-[16px];
    }
  }

  .verify-content {
    @apply relative bg-[#eee] mt-4 overflow-hidden;

    .axis {
      @apply absolute bg-[red];

      &.x-axis {
        @apply w-full h-[1px];
      }

      &.y-axis {
        @apply w-[1px] h-full;
      }
    }

    .dots {
      @apply absolute w-full h-full;

      .dot {
        @apply absolute bg-primary text-white rounded-full flex items-center justify-center text-[14px];

        border: 1px solid white;
      }
    }

    .verify-tips {
      @apply absolute bottom-0 left-0 w-full h-[36px] flex items-center justify-center text-[14px] text-white;

      &__success {
        @apply bg-[#00B324];
      }

      &__error {
        @apply bg-[#F53F3F];
      }
    }

    .loading {
      @apply absolute w-full h-full flex flex-col items-center justify-center text-white;

      background: rgba(0, 0, 0, 0.4);

      .icon {
        @apply w-4 h-[16px] mb-[8px];

        animation: loading 0.8s infinite linear;
      }
    }

    .mask {
      @apply absolute w-full h-full;
    }
  }
}
</style>

<style lang="scss">
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

.fade-enter-active,
.fade-leave-active {
  transition: all 0.5s ease;
}

.slide-bottom-enter-active,
.slide-bottom-leave-active {
  transition: all 0.3s ease-in-out;
}

.slide-bottom-enter-from,
.slide-bottom-leave-to {
  transform: translateY(100%);
}

.verify-dialog .el-dialog__body {
  padding: 28px !important;
}
</style>
